// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2025-02-27

package wechat

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"gitee.com/go-libs/log"
	"gitee.com/go-libs/redis"
	gr "github.com/go-redis/redis/v8"
	"github.com/valyala/fasthttp"
	"integration/app/lib/conf"
	"net/http"
	"sync"
	"time"
)

var (
	AccessToken AccessTokenManager
)

type (
	// AccessTokenManager
	// 微信令牌管理器.
	AccessTokenManager interface {
		// GetToken
		// 读取令牌.
		GetToken(ctx context.Context, routine *conf.ErpRoutine) (token string, err error)

		// RefreshToken
		// 刷新令牌.
		RefreshToken(ctx context.Context, routine *conf.ErpRoutine) (token string, err error)
	}

	accessToken struct {
		mu *sync.Mutex
	}

	accessResult struct {
		Token   string `json:"access_token"`
		Expires int    `json:"expires_in"`
	}
)

func (o *accessToken) GetToken(ctx context.Context, routine *conf.ErpRoutine) (token string, err error) {
	var (
		cmd  *gr.StringCmd
		conn *gr.Conn
		key  = o.generateKey(routine)
	)

	// 1. 监听结束.
	defer func() {
		if conn != nil {
			_ = conn.Close()
		}
	}()

	// 2. 获取连接.
	if conn, err = redis.Connector.GetConnection(ctx); err != nil {
		return
	}

	// 3. 读取缓存.
	if cmd = conn.Get(ctx, key); cmd.Err() == nil {
		if token = fmt.Sprintf(`%s`, cmd.Val()); token != "" {
			log.Infofc(ctx, `[weixin][token] load from cache: key="%s", token="%s"`, key, token)
			return
		}
	} else if !errors.Is(cmd.Err(), gr.Nil) {
		err = cmd.Err()
		return
	}

	// 4. 请求令牌.
	//    请求完成后, 更新缓存.
	if token, err = o.requester(ctx, routine); err == nil {
		if err = conn.Set(ctx, key, token, o.generateTimeout()).Err(); err == nil {
			log.Infofc(ctx, `[weixin][token] update cached: key="%s", token="%s"`, key, token)
		}
	}
	return
}

func (o *accessToken) RefreshToken(ctx context.Context, routine *conf.ErpRoutine) (token string, err error) {
	var conn *gr.Conn

	// 1. 监听结束.
	defer func() {
		if conn != nil {
			_ = conn.Close()
		}
	}()

	// 2. 读取令牌.
	if token, err = o.requester(ctx, routine); err != nil {
		return
	}

	// 3. 写入缓存.
	if conn, err = redis.Connector.GetConnection(ctx); err == nil {
		key := o.generateKey(routine)
		if err = conn.Set(ctx, key, token, o.generateTimeout()).Err(); err == nil {
			log.Infofc(ctx, `[weixin][token] update cached: key="%s", token="%s"`, key, token)
		}
	}
	return
}

func (o *accessToken) generateKey(routine *conf.ErpRoutine) string {
	return fmt.Sprintf(`access:token:weixin:%s`, routine.Key)
}

func (o *accessToken) generateTimeout() time.Duration {
	return time.Second * 7000
}

func (o *accessToken) requester(cts context.Context, routine *conf.ErpRoutine) (token string, err error) {
	var (
		ctx      = log.NewContextForChildInfo(cts, `[weixin][api] request access token: appid="%s"`, routine.Key)
		request  = fasthttp.AcquireRequest()
		response = fasthttp.AcquireResponse()
		result   = &accessResult{}
		url      = fmt.Sprintf(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s`, routine.Key, routine.Secret)
	)

	// 1. 监听结束.
	defer func() {
		if err != nil {
			log.Errorfc(ctx, `[wechat][api] access token failed: error="%v"`, err)
		} else {
			log.Infofc(ctx, `[wechat][api] access token done: token="%s"`, token)
		}

		fasthttp.ReleaseRequest(request)
		fasthttp.ReleaseResponse(response)
	}()

	// 2. 准备请求.
	request.Header.SetMethod(http.MethodGet)
	request.Header.SetRequestURI(url)

	// 3. 请求过程.
	if err = fasthttp.Do(request, response); err != nil {
		return
	}

	// 4. 网络错误.
	if status := response.StatusCode(); status != http.StatusOK {
		err = fmt.Errorf(`HTTP %d %s`, status, http.StatusText(status))
		return
	}

	// 5. 结果错误.
	if err = json.Unmarshal(response.Body(), result); err == nil {
		token = result.Token
	}
	return
}

func (o *accessToken) init() *accessToken {
	o.mu = &sync.Mutex{}
	return o
}
